package com.study.demo.aop.demo2.interceptor;

import com.study.demo.aop.demo2.*;
import com.study.demo.aop.demo2.operationsource.ElvinOperationSource;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.cache.interceptor.SimpleKeyGenerator;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.lang.Nullable;
import org.springframework.util.*;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author: elvin
 * @time: 2019-05-23 15:19
 * @desc:
 **/
public abstract class ElvinAspectSupport implements BeanFactoryAware {

    private final Map<ElvinAspectSupport.ElvinOperationElvinKey, ElvinAspectSupport.ElvinOperationMetadata> metadataElvin = new ConcurrentHashMap<>(1024);

    @Nullable
    private ElvinOperationSource elvinOperationSource;

    @Nullable
    private BeanFactory beanFactory;


    public void setElvinOperationSources(ElvinOperationSource... elvinOperationSources) {
        Assert.notEmpty(elvinOperationSources, "At least 1 ElvinOperationSource needs to be specified");
        this.elvinOperationSource = elvinOperationSources[0];
    }

    @Nullable
    public ElvinOperationSource getElvinOperationSource() {
        return this.elvinOperationSource;
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }

    protected String methodIdentification(Method method, Class<?> targetClass) {
        Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
        return ClassUtils.getQualifiedMethodName(specificMethod);
    }

    protected ElvinAspectSupport.ElvinOperationContext getOperationContext(
            ElvinOperation operation, Method method, Object[] args, Object target, Class<?> targetClass) {

        ElvinAspectSupport.ElvinOperationMetadata metadata = getElvinOperationMetadata(operation, method, targetClass);
        return new ElvinAspectSupport.ElvinOperationContext(metadata, args, target);
    }

    protected ElvinAspectSupport.ElvinOperationMetadata getElvinOperationMetadata(
            ElvinOperation operation, Method method, Class<?> targetClass) {

        ElvinAspectSupport.ElvinOperationElvinKey elvinKey = new ElvinAspectSupport.ElvinOperationElvinKey(operation, method, targetClass);
        ElvinAspectSupport.ElvinOperationMetadata metadata = this.metadataElvin.get(elvinKey);
        if (metadata == null) {
            metadata = new ElvinAspectSupport.ElvinOperationMetadata(operation, method, targetClass);
            this.metadataElvin.put(elvinKey, metadata);
        }
        return metadata;
    }

    @Nullable
    protected Object execute(ElvinOperationInvoker invoker, Object target, Method method, Object[] args) {

        Class<?> targetClass = getTargetClass(target);
        ElvinOperationSource elvinOperationSource = getElvinOperationSource();
        if (elvinOperationSource != null) {
            Collection<ElvinOperation> operations = elvinOperationSource.getElvinOperation(method, targetClass);
            if (!CollectionUtils.isEmpty(operations)) {
                return execute(invoker, method,
                        new ElvinAspectSupport.ElvinOperationContexts(operations, method, args, target, targetClass));
            }
        }
        return invoker.invoke();
    }

    protected Object invokeOperation(ElvinOperationInvoker invoker) {
        return invoker.invoke();
    }

    private Class<?> getTargetClass(Object target) {
        return AopProxyUtils.ultimateTargetClass(target);
    }

    @Nullable
    private Object execute(final ElvinOperationInvoker invoker, Method method, ElvinAspectSupport.ElvinOperationContexts contexts) {


        Object elvinValue;
        Object returnValue;

        // Invoke the method if we don't have a elvin hit
        System.out.println("执行方法之前...比如可以开启个事务啥的");
        returnValue = invokeOperation(invoker);
        elvinValue = unwrapReturnValue(returnValue);
        System.out.println("执行方法之后...");

        return returnValue;
    }

    @Nullable
    private Object unwrapReturnValue(Object returnValue) {
        return ObjectUtils.unwrapOptional(returnValue);
    }

    private class ElvinOperationContexts {

        private final MultiValueMap<Class<? extends ElvinOperation>, ElvinAspectSupport.ElvinOperationContext> contexts =
                new LinkedMultiValueMap<>();

        public ElvinOperationContexts(Collection<? extends ElvinOperation> operations, Method method,
                                      Object[] args, Object target, Class<?> targetClass) {

            for (ElvinOperation operation : operations) {
                this.contexts.add(operation.getClass(), getOperationContext(operation, method, args, target, targetClass));
            }
        }
    }

    protected static class ElvinOperationMetadata {

        private final ElvinOperation operation;

        private final Method method;

        private final Class<?> targetClass;

        private final Method targetMethod;

        private final AnnotatedElementKey methodKey;

        public ElvinOperationMetadata(ElvinOperation operation, Method method, Class<?> targetClass) {

            this.operation = operation;
            this.method = BridgeMethodResolver.findBridgedMethod(method);
            this.targetClass = targetClass;
            this.targetMethod = (!Proxy.isProxyClass(targetClass) ?
                    AopUtils.getMostSpecificMethod(method, targetClass) : this.method);
            this.methodKey = new AnnotatedElementKey(this.targetMethod, targetClass);
        }
    }


    protected class ElvinOperationContext implements ElvinOperationInvocationContext<ElvinOperation> {

        private final ElvinAspectSupport.ElvinOperationMetadata metadata;

        private final Object[] args;

        private final Object target;

        public ElvinOperationContext(ElvinAspectSupport.ElvinOperationMetadata metadata, Object[] args, Object target) {
            this.metadata = metadata;
            this.args = extractArgs(metadata.method, args);
            this.target = target;
        }

        @Override
        public ElvinOperation getOperation() {
            return this.metadata.operation;
        }

        @Override
        public Object getTarget() {
            return this.target;
        }

        @Override
        public Method getMethod() {
            return this.metadata.method;
        }

        @Override
        public Object[] getArgs() {
            return this.args;
        }

        private Object[] extractArgs(Method method, Object[] args) {
            if (!method.isVarArgs()) {
                return args;
            }
            Object[] varArgs = ObjectUtils.toObjectArray(args[args.length - 1]);
            Object[] combinedArgs = new Object[args.length - 1 + varArgs.length];
            System.arraycopy(args, 0, combinedArgs, 0, args.length - 1);
            System.arraycopy(varArgs, 0, combinedArgs, args.length - 1, varArgs.length);
            return combinedArgs;
        }
    }


    private static final class ElvinOperationElvinKey implements Comparable<ElvinAspectSupport.ElvinOperationElvinKey> {

        private final ElvinOperation elvinOperation;

        private final AnnotatedElementKey methodElvinKey;

        private ElvinOperationElvinKey(ElvinOperation elvinOperation, Method method, Class<?> targetClass) {
            this.elvinOperation = elvinOperation;
            this.methodElvinKey = new AnnotatedElementKey(method, targetClass);
        }

        @Override
        public boolean equals(Object other) {
            if (this == other) {
                return true;
            }
            if (!(other instanceof ElvinAspectSupport.ElvinOperationElvinKey)) {
                return false;
            }
            ElvinAspectSupport.ElvinOperationElvinKey otherKey = (ElvinAspectSupport.ElvinOperationElvinKey) other;
            return (this.elvinOperation.equals(otherKey.elvinOperation) &&
                    this.methodElvinKey.equals(otherKey.methodElvinKey));
        }

        @Override
        public int hashCode() {
            return (this.elvinOperation.hashCode() * 31 + this.methodElvinKey.hashCode());
        }

        @Override
        public String toString() {
            return this.elvinOperation + " on " + this.methodElvinKey;
        }

        @Override
        public int compareTo(ElvinAspectSupport.ElvinOperationElvinKey other) {
            int result = this.elvinOperation.getName().compareTo(other.elvinOperation.getName());
            if (result == 0) {
                result = this.methodElvinKey.compareTo(other.methodElvinKey);
            }
            return result;
        }
    }

}
